﻿using System;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;
using System.ComponentModel;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Represents a clef.
    /// </summary>
    /// <remarks>
    /// <para>
    /// A clef is a musical symbol used to indicate the pitch of written notes.
    /// Placed on one of the lines at the beginning of the staff, it indicates the name and pitch of the notes on that line. 
    /// This line serves as a reference point by which the names of the notes on any other line or space of the staff may be determined.
    /// </para>
    /// <para>
    /// There are three types of clef used in modern music notation: F, C, and G. 
    /// Each type of clef assigns a different reference note to the line on which it is placed.
    /// </para>
    /// </remarks>
    public class Clef : EnvironmentModifier
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Clef"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="type">
        /// <para>
        /// Type: <see cref="ClefType"/>
        /// </para>
        /// <para>
        /// The type of the clef.
        /// </para>
        /// </param>
        /// <param name="measure">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure that this clef logically belongs to.
        /// </para>
        /// </param>
        /// <param name="beat">
        /// <para>
        /// Type: <see cref="BeatElement"/>
        /// </para>
        /// <para>
        /// The beat that this symbol logically belongs to.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// The <paramref name="measure"/> is <see langword="null"/>.
        /// </exception>
        internal Clef(XmlPackagePart packagePart, Staff owner, ClefType type, MeasureElement measure, BeatElement beat)
            : base(packagePart, owner, measure, beat)
        {
            typeAttribute.Value = type.ToString();
            ComputeClefTone();

            PropertyChanged += new PropertyChangedEventHandler(Clef_PropertyChanged);
        }

        /// <summary>
        /// Creates a new <see cref="Clef"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal Clef(XmlPackagePart packagePart, Staff owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            typeAttribute = element.Attributes[PackageVocabulary.ClefTypeAttribute];
            ComputeClefTone();

            PropertyChanged += new PropertyChangedEventHandler(Clef_PropertyChanged);
        }

        #endregion

        #region CoreSymbol Members

        /// <inheritdoc/>
        public override double Width
        {
            get 
            {
                return FindOwner<Staff>().SpaceSize * 3;
            }
        }

        /// <inheritdoc/>
        public override void ProcessEnvironmentChange(Environment environment)
        {
            
        }

        #endregion

        #region EnvironmentModifier Members

        /// <inheritdoc/>
        protected override XmlElement CreateSymbolElementCore(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.ClefElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            typeAttribute = document.CreateAttribute(PackageVocabulary.ClefTypeAttribute);

            element.Attributes.Append(typeAttribute);

            return element;
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the value of some property of this object has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// If the modified property is <see cref="CoreSymbol.StaffPosition"/> 
        /// or <see cref="Clef.Type"/> the handler raises the 
        /// <see cref="CoreSymbol.EnvironmentChanged"/> event.
        /// </remarks>
        /// <param name="sender">The object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Clef_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "StaffPosition" || e.PropertyName == "Type")
                RaiseEnvironmentChanged();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Sets the <see cref="clefNote"/> and <see cref="clefOctave"/> fields
        /// according to the current clef <see cref="Type"/>.
        /// </summary>
        private void ComputeClefTone()
        {
            switch (Type)
            {
                case ClefType.C:
                    clefNote = 1;
                    clefOctave = 0;
                    break;
                case ClefType.F:
                    clefNote = 4;
                    clefOctave = -1;
                    break;
                case ClefType.G:
                    clefNote = 5;
                    clefOctave = 0;
                    break;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Computes the staff position that corresponds to a given tone.
        /// </summary>
        /// <remarks>
        /// <para>
        /// The position is computed based on the <see cref="ToneElement.Note"/> and <see cref="ToneElement.Octave"/> 
        /// (the <see cref="ToneElement.Accidental"/> value is ignored in this calculation).
        /// </para>
        /// <para>
        /// The algorithm is simple:
        /// <list type="number">
        /// <item>
        ///     The relative position of the <see cref="ToneElement.Note"/> and the note indicated by this clef is computed
        ///     as a subtraction: <see cref="ToneElement.Note"/> - <see cref="Clef"/>'s note.<br/>
        ///     E.g.: G-clef, Tone C - relative position equals 4 (the note is 2 staff lines bellow the clef position).
        /// </item>
        /// <item>
        ///     The subtraction: <see cref="ToneElement.Octave"/> - <see cref="Clef"/>'s note octave is computed giving
        ///     the difference in the octaves including the direction.<br/>
        ///     E.g.: Treble-clef, Tone C5 - octave difference is +1
        /// </item>
        /// <item>
        ///     The final position corresponding to the tone is computed as follows:<br/>
        ///     Clef's <see cref="CoreSymbol.StaffPosition"/> + Relative tones position + Octaves difference * 7<br/>
        ///     E.g.: Treble-clef, Tone E5:<br/>
        ///     <list type="bullet">
        ///     <item>
        ///         Relative tones position: E - G = -2
        ///     </item>
        ///     <item>
        ///         Octaves difference: +1
        ///     </item>
        ///     <item>
        ///         Treble-clef's <see cref="CoreSymbol.StaffPosition"/> = 2 (second staff line)
        ///     </item>
        ///     <item>
        ///         <b>Final position:</b> 2 + (-2) + 1 * 7 = 7 (the space between 4th and 5th staff line).
        ///     </item>
        ///     </list>
        /// </item>
        /// </list>
        /// </para>
        /// </remarks>
        /// <param name="tone">
        /// <para>
        /// Type: <see cref="Tone"/>
        /// </para>
        /// <para>
        /// The tone for which the position is to be calculated.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The position on the staff that corresponds to the tone.
        /// </para>
        /// </returns>
        public int GetPositionFromTone(Tone tone)
        {
            int relativePos = tone.Note - clefNote;
            int octavesDist = tone.Octave - clefOctave;

            return StaffPosition + relativePos + octavesDist * 7;
        }

        /// <summary>
        /// Computes the tone that corresponds to a given staff position.
        /// </summary>
        /// <remarks>
        /// The algorithm is just a reversed version of <see cref="GetPositionFromTone"/>, so please
        /// refer to the documentation of the <see cref="GetPositionFromTone"/> for details).
        /// </remarks>
        /// <param name="position">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The staff position for which the tone is being computed.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Tone"/>
        /// </para>
        /// <para>
        /// The tone that corresponds to the given staff position.
        /// </para>
        /// </returns>
        public Tone GetToneFromPosition(int position)
        {
            int distance = position - StaffPosition;
            int octavesDist = distance / 7;
            int relativePos = distance % 7;

            Tone tone = new Tone();

            int note = relativePos + clefNote;
            tone.Octave = octavesDist + clefOctave;
            if (note > 7)
            {
                note -= 7;
                ++tone.Octave;
            }
            else if (note < 1)
            {
                note += 7;
                --tone.Octave;
            }
            tone.Note = (byte)note;

            return tone;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the type of the clef.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ClefType"/>
        /// </para>
        /// <para>
        /// One of the <see cref="ClefType"/> values.
        /// </para>
        /// </value>
        public ClefType Type
        {
            get { return (ClefType)Enum.Parse(typeof(ClefType), typeAttribute.Value); }
            set
            {
                typeAttribute.Value = value.ToString();
                ComputeClefTone();

                RaisePropertyChanged("Type");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the note indicated by this clef.
        /// </summary>
        private byte clefNote;
        /// <summary>
        /// Holds the octave indicated by this clef.
        /// </summary>
        private int clefOctave;
        /// <summary>
        /// Holds the value of the <see cref="Type"/> property.
        /// </summary>
        private XmlAttribute typeAttribute;

        #endregion
    }

    /// <summary>
    /// Describes the type of a clef.
    /// </summary>
    /// <seealso cref="Clef"/>
    public enum ClefType
    {
        /// <summary>
        /// G-clef assigns the G4 note to the line on which it is placed.
        /// </summary>
        G,
        /// <summary>
        /// C-clef assigns the middle C note to the line on which it is placed.
        /// </summary>
        C,
        /// <summary>
        /// F-clef assigns the F3 note to the line on which it is placed.
        /// </summary>
        F
    }
}
